/**
 * This class is the base class for both {@link Ext.tree.Panel TreePanel} and
 * {@link Ext.grid.Panel GridPanel}.
 *
 * TablePanel aggregates:
 *
 *  - a Selection Model
 *  - a View
 *  - a Store
 *  - Ext.grid.header.Container
 * 
 * @mixins Ext.grid.locking.Lockable
 */
Ext.define('Ext.panel.Table', {
    extend: 'Ext.panel.Panel',

    xtype: 'tablepanel',

    requires: [
        'Ext.layout.container.Fit'
    ],

    uses: [
        'Ext.selection.RowModel',
        'Ext.selection.CellModel',
        'Ext.selection.CheckboxModel',
        'Ext.grid.plugin.BufferedRenderer',
        'Ext.grid.header.Container',
        'Ext.grid.locking.Lockable',
        'Ext.grid.NavigationModel',
        'Ext.grid.RowContext',
        'Ext.grid.CellContext'
    ],

    extraBaseCls: Ext.baseCSSPrefix + 'grid',
    extraBodyCls: Ext.baseCSSPrefix + 'grid-body',
    actionableModeCls: Ext.baseCSSPrefix + 'grid-actionable',
    noHeaderBordersCls: Ext.baseCSSPrefix + 'no-header-borders',

    /**
     * @property defaultBindProperty
     * @inheritdoc
     */
    defaultBindProperty: 'store',

    /**
     * @cfg layout
     * @inheritdoc
     */
    layout: 'fit',

    manageLayoutScroll: false,

    /**
     * @property ariaRole
     * @inheritdoc
     */
    ariaRole: 'presentation',

    config: {
        /**
         * @cfg {Ext.grid.CellContext/Ext.data.Model/Number} focused
         * The focused cell, model or index. Typically used with {@link #bind binding}.
         *
         * If bound to a record (such as a selection), the first cell will be focused.
         */
        focused: null,

        /**
         * @cfg {Boolean} headerBorders
         * To show no borders around grid headers, configure this as `false`.
         */
        headerBorders: true,

        /**
         * @cfg {Boolean} hideHeaders
         * By default, visibility of headers is managed automatically based upon
         * whether there is textual content to display.
         * This configuration is only necessary if you want to disable automatic
         * header visibility management.
         *
         * If no columns have a {@link Ext.grid.column.Column#title text} config
         * (for example in the case of a {@link Ext.tree.Panel TreePanel} with no
         * columns specified), and no columns have
         * {@link Ext.grid.column.Column#columns child columns} then headers are hidden.
         *
         * If this status changes - if the column set ever goes from none having
         * text, to one having text or vice versa), then the visibility of headers
         * will be recalculated.
         *
         * Configure as `true` to hide column headers. Configure as `false` to show
         * column headers even if none of them have text.
         *
         */
        hideHeaders: null
    },

    /**
     * @cfg publishes
     * @inheritdoc
     */
    publishes: ['selection'],
    /**
     * @cfg twoWayBindable
     * @inheritdoc
     */
    twoWayBindable: ['selection'],

    /**
     * @cfg {Ext.data.Model} selection
     * The selected model. Typically used with {@link #bind binding}.
     */
    selection: null,

    /**
     * @cfg {Boolean} autoLoad
     * Use `true` to load the store as soon as this component is fully constructed. It is
     * best to initiate the store load this way to allow this component and potentially
     * its plugins (such as `{@link Ext.grid.filters.Filters}`) to be ready to load.
     */
    autoLoad: false,

    /**
     * @cfg {Boolean} variableRowHeight
     * @deprecated 5.0.0 Use {@link Ext.grid.column.Column#variableRowHeight} instead.
     * Configure as `true` if the row heights are not all the same height as the first row.
     */
    variableRowHeight: false,

    /**
     * @cfg {Number} numFromEdge
     * This configures the zone which causes new rows to be appended to the view. As soon
     * as the edge of the rendered grid is this number of rows from the edge of the viewport,
     * the view is moved.
     */
    numFromEdge: 2,

    /**
     * @cfg {Number} trailingBufferZone
     * TableViews are buffer rendered in 5.x and above which means that only the visible subset
     * of data rows are rendered into the DOM. These are removed and added as scrolling demands.
     *
     * This configures the number of extra rows to render on the trailing side of scrolling
     * **outside the {@link #numFromEdge}** buffer as scrolling proceeds.
     */
    trailingBufferZone: 10,

    /**
     * @cfg {Number} [leadingBufferZone]
     * TableViews are buffer rendered in 5.x and above which means that only the visible subset
     * of data rows are rendered into the DOM. These are removed and added as scrolling demands.
     *
     * This configures the number of extra rows to render on the leading side of scrolling
     * **outside the {@link #numFromEdge}** buffer as scrolling proceeds.
     */
    leadingBufferZone: 20,

    /**
     * @property {Boolean} hasView
     * True to indicate that a view has been injected into the panel.
     */
    hasView: false,

    /**
     * @property items
     * @hide
     */

    /**
     * @cfg {String} viewType
     * An xtype of view to use. This is automatically set to 'tableview' by
     * {@link Ext.grid.Panel Grid} and to 'treeview' by {@link Ext.tree.Panel Tree}.
     * @protected
     */
    viewType: null,

    /**
     * @cfg {Object} viewConfig
     * A config object that will be applied to the grid's UI view. Any of the config options
     * available for {@link Ext.view.Table} can be specified here. This option is ignored
     * if {@link #view} is specified.
     */

    /**
     * @cfg {String/Object} rowViewModel
     * The type or a config object specifying the type of the ViewModel to instantiate when creating
     * ViewModels for records to which {@link Ext.grid.column.Widget widgets in widget columns},
     * and widgets in a {@link Ext.grid.plugin.RowWidget RowWidget} row bind.
     */

    /**
     * @cfg {Ext.view.Table} view
     * The {@link Ext.view.Table} used by the grid. Use {@link #viewConfig} to just supply
     * some config options to view (instead of creating an entire View instance).
     */

    /**
     * @cfg {String} [selType]
     * An xtype of selection model to use. This is used to create selection model if just
     * a config object or nothing at all given in {@link #selModel} config.
     *
     * @deprecated 5.1.0 Use the {@link #selModel}'s `type` property. Or, if no other
     * configs are required, use the string form of selModel.
     */

    /**
     * @cfg {Ext.selection.Model/Object/String} [selModel=rowmodel]
     * A {@link Ext.selection.Model selection model} instance or config object, or the selection
     * model class's alias string.
     *
     * In latter case its `type` property determines to which type of selection model this config
     * is applied.
     */

    /**
     * @cfg {Boolean} [multiSelect=false]
     * True to enable 'MULTI' selection mode on selection model.
     * @deprecated 4.1.1 Use {@link Ext.selection.Model#mode} 'MULTI' instead.
     */

    /**
     * @cfg {Boolean} [simpleSelect=false]
     * True to enable 'SIMPLE' selection mode on selection model.
     * @deprecated 4.1.1 Use {@link Ext.selection.Model#mode} 'SIMPLE' instead.
     */

    /**
     * @cfg {Ext.data.Store/String/Object} store (required)
     * The data source to which the grid / tree is bound. Acceptable values for this 
     * property are:
     *
     *   - **any {@link Ext.data.Store Store} class / subclass**
     *   - **an {@link Ext.data.Store#storeId ID of a store}**
     *   - **a {@link Ext.data.Store Store} config object**.  When passing a config you can 
     *   specify the store type by alias.  Passing a config object with a store type will 
     *   dynamically create a new store of that type when the grid / tree is instantiated.
     *
     * For example:
     * 
     *     Ext.define('MyApp.store.Customers', {
     *         extend: 'Ext.data.Store',
     *         alias: 'store.customerstore',
     *         fields: ['name']
     *     });
     *     
     *     Ext.create({
     *         xtype: 'gridpanel',
     *         renderTo: document.body,
     *         store: {
     *             type: 'customerstore',
     *             data: [{
     *                 name: 'Foo'
     *             }]
     *         },
     *         columns: [{
     *             text: 'Name',
     *             dataIndex: 'name'
     *         }]
     *     });
     */

    /**
     * @cfg {String/Boolean} scroll
     * Scrollers configuration. Valid values are 'both', 'horizontal' or 'vertical'.
     * True implies 'both'. False implies 'none'.
     * @deprecated 5.1.0 Use {@link #scrollable} instead
     */

    /**
     * @cfg {Boolean} [reserveScrollbar=false]
     * Set this to true to **always** leave a scrollbar sized space at the end of the grid content
     * when fitting content into the width of the grid.
     *
     * If the grid's record count fluctuates enough to hide and show the scrollbar regularly,
     * this setting avoids the multiple layouts associated with switching from scrollbar present
     * to scrollbar not present.
     */

    /**
     * @cfg {Ext.grid.column.Column[]/Object} columns
     * An array of {@link Ext.grid.column.Column column} definition objects which define all columns
     * that appear in this grid. Each column definition provides the header text for the column,
     * and a definition of where the data for that column comes from.
     *
     * This can also be a configuration object for a
     * {@link Ext.grid.header.Container HeaderContainer} which may override certain default
     * configurations if necessary. For example, the special layout may be overridden to use
     * a simpler layout, or one can set default values shared by all columns:
     * 
     *     columns: {
     *         items: [
     *             {
     *                 text: "Column A",
     *                 dataIndex: "field_A"
     *             }, {
     *                 text: "Column B",
     *                 dataIndex: "field_B"
     *             }, 
     *             ...
     *         ],
     *         defaults: {
     *             flex: 1
     *         }
     *     }
     */

    /**
     * @cfg {Boolean} forceFit
     * True to force the columns to fit into the available width. Headers are first sized according
     * to configuration, whether that be a specific width, or flex. Then they are all proportionally
     * changed in width so that the entire content width is used. For more accurate control,
     * it is more optimal to specify a flex setting on the columns that are to be stretched
     * and explicit widths on columns that are not.
     */

    /**
     * @cfg {Ext.grid.feature.Feature[]/Object[]/Ext.enums.Feature[]} features
     * An array of grid Features to be added to this grid. Can also be just a single feature
     * instead of array.
     *
     * Features config behaves much like {@link #plugins}.
     * A feature can be added by either directly referencing the instance:
     *
     *     features: [
     *         Ext.create('Ext.grid.feature.GroupingSummary', {groupHeaderTpl: 'Subject: {name}'})
     *     ],
     *
     * By using config object with ftype:
     *
     *     features: [{ftype: 'groupingsummary', groupHeaderTpl: 'Subject: {name}'}],
     *
     * Or with just a ftype:
     *
     *     features: ['grouping', 'groupingsummary'],
     *
     * See {@link Ext.enums.Feature} for list of all ftypes.
     */

    /**
     * @cfg {Boolean} deferRowRender
     * Configure as `true` to enable deferred row rendering.
     *
     * This allows the View to execute a refresh quickly, with the update of the row structure
     * deferred so that layouts with GridPanels appear, and lay out more quickly.
     */
    deferRowRender: false,

    /**
     * @cfg {Boolean} sortableColumns
     * False to disable column sorting via clicking the header and via the Sorting menu items.
     */
    sortableColumns: true,

    /**
     * @cfg {Boolean} multiColumnSort
     * Configure as `true` to have columns remember their sorted state after other columns have been
     * clicked upon to sort.
     *
     * As subsequent columns are clicked upon, they become the new primary sort key.
     *
     * The maximum number of sorters allowed in a Store is configurable via its underlying data
     * collection. See {@link Ext.util.Collection#multiSortLimit}
     */
    multiColumnSort: false,

    /**
     * @cfg {Boolean} enableLocking
     * Configure as `true` to enable locking support for this grid. Alternatively, locking will also
     * be automatically enabled if any of the columns in the {@link #columns columns} configuration
     * contain a {@link Ext.grid.column.Column#locked locked} config option.
     * 
     * A locking grid is processed in a special way. The configuration options are cloned and *two*
     * grids are created to be the locked (left) side and the normal (right) side. This Panel
     * becomes merely a {@link Ext.container.Container container} which arranges both in an
     * {@link Ext.layout.container.HBox HBox} layout.
     * 
     * {@link #plugins Plugins} may be targeted at either locked, or unlocked grid, or, both,
     * in which case the plugin is cloned and used on both sides.
     * 
     * Plugins may also be targeted at the containing locking Panel.
     * 
     * This is configured by specifying a `lockableScope` property in your plugin which may have
     * the following values:
     * 
     *  * `"both"` (the default) - The plugin is added to both grids
     *  * `"top"` - The plugin is added to the containing Panel
     *  * `"locked"` - The plugin is added to the locked (left) grid
     *  * `"normal"` - The plugin is added to the normal (right) grid
     *
     * If `both` is specified, then each copy of the plugin gains a property `lockingPartner`
     * which references its sibling on the other side so that they can synchronize operations
     * if necessary.
     * 
     * {@link #features Features} may also be configured with `lockableScope` and may target
     * the locked grid, the normal grid or both grids. Features also get a `lockingPartner`
     * reference injected.
     */
    enableLocking: false,

    /**
     * @private
     * Used to determine where to go down to find views
     * this is here to support locking.
     */
    scrollerOwner: true,

    /**
     * @cfg {Boolean} enableColumnMove
     * False to disable column dragging within this grid.
     */
    enableColumnMove: true,

    /**
     * @cfg {Boolean} sealedColumns
     * True to constrain column dragging so that a column cannot be dragged in or out of it's
     * current group. Only relevant while {@link #enableColumnMove} is enabled.
     */
    sealedColumns: false,

    /**
     * @cfg {Boolean} enableColumnResize
     * False to disable column resizing within this grid.
     */
    enableColumnResize: true,

    /**
     * @cfg {Boolean} [enableColumnHide=true]
     * False to disable column hiding within this grid.
     */

    /**
     * @cfg {Boolean} columnLines
     * Adds column line styling
     */
    columnLines: false,

    /**
     * @cfg {Boolean} rowLines
     * Adds row line styling
     */
    rowLines: true,

    /**
     * @cfg {Boolean} [disableSelection=false]
     * True to disable selection model.
     */

    /**
     * @cfg {String} emptyText Default text (HTML tags are accepted) to display in the 
     * Panel body when the Store is empty. When specified, and the Store is empty, the 
     * text will be rendered inside a DIV with the CSS class "x-grid-empty". The emptyText 
     * will not display until the first load of the associated store by default. If you 
     * want the text to be displayed prior to the first store load use the 
     * {@link Ext.view.Table#deferEmptyText deferEmptyText} config in the {@link #viewConfig}
     * config.
     */

    /**
     * @cfg {Boolean} [allowDeselect=false]
     * True to allow deselecting a record. This config is forwarded to
     * {@link Ext.selection.Model#allowDeselect}.
     */

    /**
     * @cfg {Boolean} bufferedRenderer
     * Buffered rendering is enabled by default.
     * 
     * Configure as `false` to disable buffered rendering.
     * See {@link Ext.grid.plugin.BufferedRenderer}.
     *
     * @since 5.0.0
     */
    bufferedRenderer: true,

    /**
     * @cfg {Boolean} preciseHeight
     * Set to `true` to ensure that measurements (such as locking grid's row-height synchronization)
     * accurately measure rows with sub-pixel sizes. This can be an issue for some types
     * of row content on browsers that support sub-pixel sizing. Note that setting this to `true`
     * may cause a decrease in performance for large amounts of rendered content and therefore
     * should only be used when needed.
     * @since 6.5.1
     */
    preciseHeight: false,

    /**
     * @cfg stateEvents
     * @inheritdoc Ext.state.Stateful#cfg-stateEvents
     * @localdoc By default the following stateEvents are added:
     * 
     *  - {@link #event-resize} - _(added by Ext.Component)_
     *  - {@link #event-collapse} - _(added by Ext.panel.Panel)_
     *  - {@link #event-expand} - _(added by Ext.panel.Panel)_
     *  - {@link #event-columnresize}
     *  - {@link #event-columnmove}
     *  - {@link #event-columnhide}
     *  - {@link #event-columnshow}
     *  - {@link #event-sortchange}
     *  - {@link #event-filterchange}
     *  - {@link #event-groupchange}
     */

    /**
     * @property {Boolean} optimizedColumnMove
     * If you are writing a grid plugin or a {Ext.grid.feature.Feature Feature} which creates
     * a column-based structure which needs a view refresh when columns are moved, then set
     * this property in the grid.
     *
     * An example is the built in {@link Ext.grid.feature.AbstractSummary Summary} Feature.
     * This creates summary rows, and the summary columns must be in the same order
     * as the data columns. This plugin sets the `optimizedColumnMove` to `false.
     */

    /**
     * @property {Ext.panel.Table} ownerGrid
     * A reference to the top-level owning grid component.
     * 
     * This is a reference to this GridPanel if this GridPanel is not part of a locked grid
     * arrangement.
     * @readonly
     * @private
     * @since 5.0.0
     */
    ownerGrid: null,

    colLinesCls: Ext.baseCSSPrefix + 'grid-with-col-lines',
    rowLinesCls: Ext.baseCSSPrefix + 'grid-with-row-lines',
    noRowLinesCls: Ext.baseCSSPrefix + 'grid-no-row-lines',
    hiddenHeaderCtCls: Ext.baseCSSPrefix + 'grid-header-ct-hidden',
    hiddenHeaderCls: Ext.baseCSSPrefix + 'grid-header-hidden',
    resizeMarkerCls: Ext.baseCSSPrefix + 'grid-resize-marker',
    emptyCls: Ext.baseCSSPrefix + 'grid-empty',

    // The TablePanel claims to be focusable, but it does not place a tabIndex
    // on any of its elements.
    // Its focus implementation delegates to its view. TableViews are focusable.
    /**
     * @property focusable
     * @inheritdoc
     */
    focusable: true,

    /**
     * @event viewready
     * Fires when the grid view is available (use this for selecting a default row).
     * @param {Ext.panel.Table} this
     */

    constructor: function(config) {
        var me = this,
            topGrid = config && config.ownerGrid,
            store;

        me.ownerGrid = topGrid || me;

        /**
         * @property {Array} actionables An array of objects which register themselves
         * with a grid panel using {@link #registerActionable} which are consulted upon entry
         * into actionable mode.
         *
         * These must implement the following methods:
         *
         *    - activateCell Called when actionable mode is requested upon a cell.
         *    A {@link Ext.grid.CellContext CellContext} object is passed. If that cell
         *    is actionable by the terms of the callee, the callee should return `true` if it
         *    ascertains that the cell is actionable, and that it now contains focusable elements
         *    which may be tabbed to. 
         *    - activateRow Called when the user enters actionable mode in a row. The row DOM
         *    is passed. Actionables should take any action they need to prime the row for cell
         *    activation which happens as users TAB from cell to cell.
         * @readonly
         */
        // One shared array when there's a lockable at the top
        me.actionables = topGrid ? topGrid.actionables : [];

        me.callParent([config]);

        store = me.store;

        // Any further changes become stateful.
        store.trackStateChanges = true;

        if (me.autoLoad) {
            // Note: if there is a store bound by a VM, we (might) do the load in #setStore.
            if (!store.isEmptyStore) {
                store.load();
            }
        }
    },

    /**
     * 
     * @param {Object} actionable An object which has an interest in the implementation
     * of actionable mode in this grid.
     *
     * An actionable object may be a Plugin which upon activation injects tabbable elements
     * or Components into a grid row.
     */
    registerActionable: function(actionable) {
        // If a lockableScope: 'both' plugin/feature registers on each side,
        // only include it in the actionables once.
        Ext.Array.include(this.actionables, actionable);
    },

    initComponent: function() {
        //<debug>
        if (this.verticalScroller) {
            Ext.raise("The verticalScroller config is not supported.");
        }

        if (!this.viewType) {
            Ext.raise("You must specify a viewType config.");
        }

        if (this.headers) {
            Ext.raise("The headers config is not supported. Please specify columns instead.");
        }
        //</debug>

        // eslint-disable-next-line vars-on-top
        var me = this,
            columns = me.columns || me.colModel || [],
            selection = me.selection,
            store, view, i, len, bufferedRenderer, headerCtCfg, headerCt;

        if (selection) {
            me.selection = null;
            me.setSelection(selection);
        }

        // Look up the configured Store. If none configured, use the fieldless, empty Store
        // defined in Ext.data.Store. If store configuration is present with no storeId
        // we will be creating a new Store instance unique to this Panel, and we should
        // destroy it as well.
        store = me.store;

        if (store && Ext.isObject(store) && !store.isStore && !store.storeId) {
            store = Ext.apply({
                autoDestroy: true
            }, store);
        }

        store = me.store = Ext.data.StoreManager.lookup(store || 'ext-empty-store');

        me.enableLocking = me.enableLocking || me.hasLockedColumns(columns);

        // Construct the plugins now rather than in the constructor of AbstractComponent
        // because the component may have a subclass that has overridden initComponent
        // and defined plugins in it. For plugins like RowExpander that rely upon a grid feature,
        // this is a problem because the view needs to know about all its features before it's
        // constructed. Constructing the plugins now ensures that plugins defined in the instance
        // config or in initComponent are all constructed before the view.
        // See EXTJSIV-11927.
        //
        // Note that any components that do not inherit from this class will still have
        // their plugins constructed in AbstractComponent#initComponent.
        if (me.plugins) {
            me.plugins = me.constructPlugins();
        }

        // Add the row/column line classes to the body element so that the settings are not
        // inherited by docked grids (https://sencha.jira.com/browse/EXTJSIV-9263).
        if (me.columnLines) {
            me.addBodyCls(me.colLinesCls);
        }

        me.addBodyCls(me.rowLines ? me.rowLinesCls : me.noRowLinesCls);
        me.addBodyCls(me.extraBodyCls);

        // If any of the Column objects contain a locked property, and are not processed,
        // this is a lockable TablePanel, a special view will be injected by the
        // Ext.grid.locking.Lockable mixin, so no processing of.
        if (me.enableLocking) {
            // Only first invocation mixes Lockable into the TablePanel class
            if (!me.mixins.lockable) {
                me.self.mixin('lockable', Ext.grid.locking.Lockable);
            }

            me.injectLockable();
        }
        // Not lockable - create the HeaderContainer
        else {
            // It's a fully instantiated HeaderContainer
            if (columns.isRootHeader) {
                me.headerCt = headerCt = columns;
                headerCt.grid = me;
                headerCt.forceFit = !!me.forceFit;
                columns = [];

                // If it's an instance then the column managers were already created and bound
                // to the headerCt.
                me.columnManager = headerCt.columnManager;
                me.visibleColumnManager = headerCt.visibleColumnManager;
            }
            // It's an array of Column definitions, or a config object of a HeaderContainer
            else {
                headerCtCfg = {
                    grid: me,
                    $initParent: me,
                    forceFit: me.forceFit,
                    sortable: me.sortableColumns,
                    enableColumnMove: me.enableColumnMove,
                    enableColumnResize: me.enableColumnResize,
                    columnLines: me.columnLines,
                    sealed: me.sealedColumns
                };

                if (Ext.isObject(columns)) {
                    Ext.apply(headerCtCfg, columns);
                    columns = columns.items;
                    delete headerCtCfg.items;
                }

                me.headerCt = headerCt = new Ext.grid.header.Container(headerCtCfg);
            }

            if (Ext.isDefined(me.enableColumnHide)) {
                headerCt.enableColumnHide = me.enableColumnHide;
            }
        }

        me.scrollTask = new Ext.util.DelayedTask(me.syncHorizontalScroll, me);

        me.cls = (me.cls || '') + (' ' + me.extraBaseCls);

        // autoScroll is not a valid configuration
        delete me.autoScroll;

        bufferedRenderer = me.plugins && Ext.Array.findBy(me.plugins, function(p) {
            return p.isBufferedRenderer;
        });

        // If we find one in the plugins, just use that.
        if (bufferedRenderer) {
            me.bufferedRenderer = bufferedRenderer;
        }

        // If this TablePanel is lockable (Either configured lockable, or any of the defined 
        // columns has a 'locked' property) then a special lockable view containing 2 side-by-side
        // grids will have been injected so we do not need to set up any UI.
        if (!me.hasView) {

            // If the store is paging blocks of the dataset in, then it can only be sorted remotely.
            // And if the store is not remoteSort, then we cannot sort it at all.
            if (store.isBufferedStore && !store.getRemoteSort()) {
                for (i = 0, len = columns.length; i < len; i++) {
                    columns[i].sortable = false;
                }
            }

            me.relayHeaderCtEvents(headerCt);
            me.features = me.features || [];

            if (!Ext.isArray(me.features)) {
                me.features = [me.features];
            }

            me.viewConfig = me.viewConfig || {};

            // AbstractDataView will look up a Store configured as an object
            // getView converts viewConfig into a View instance
            view = me.getView();

            me.items = [view];
            me.hasView = true;

            // Attach this Panel to the Store
            me.bindStore(store, true);

            me.mon(view, {
                viewready: me.onViewReady,
                refresh: me.onRestoreHorzScroll,
                scope: me
            });
        }

        // Whatever kind of View we have, be it a TableView, or a LockingView, we are interested
        // in the selection model
        me.selModel = me.view.getSelectionModel();

        // We update the bound selection whenever the selectionchange event fires.
        // Even a CellModel, or a SpreadsheetModel in cell selection mode can yield
        // the *records* that are selected, and it is the first record which is published
        // to the selection property.
        me.selModel.on({
            scope: me,
            lastselectedchanged: me.updateBindSelection,
            selectionchange: me.updateBindSelection
        });

        // Relay events from the View whether it be a LockingView, or a regular GridView
        me.relayEvents(me.view, [
            /**
             * @event beforeitemlongpress
             * @inheritdoc Ext.view.View#beforeitemlongpress
             */
            'beforeitemlongpress',

            /**
             * @event beforeitemmousedown
             * @inheritdoc Ext.view.View#beforeitemmousedown
             */
            'beforeitemmousedown',

            /**
             * @event beforeitemmouseup
             * @inheritdoc Ext.view.View#beforeitemmouseup
             */
            'beforeitemmouseup',

            /**
             * @event beforeitemmouseenter
             * @inheritdoc Ext.view.View#beforeitemmouseenter
             */
            'beforeitemmouseenter',

            /**
             * @event beforeitemmouseleave
             * @inheritdoc Ext.view.View#beforeitemmouseleave
             */
            'beforeitemmouseleave',

            /**
             * @event beforeitemclick
             * @inheritdoc Ext.view.View#beforeitemclick
             */
            'beforeitemclick',

            /**
             * @event beforeitemdblclick
             * @inheritdoc Ext.view.View#beforeitemdblclick
             */
            'beforeitemdblclick',

            /**
             * @event beforeitemcontextmenu
             * @inheritdoc Ext.view.View#beforeitemcontextmenu
             */
            'beforeitemcontextmenu',

            /**
             * @event itemlongpress
             * @inheritdoc Ext.view.View#itemlongpress
             */
            'itemlongpress',

            /**
             * @event itemmousedown
             * @inheritdoc Ext.view.View#itemmousedown
             */
            'itemmousedown',

            /**
             * @event itemmouseup
             * @inheritdoc Ext.view.View#itemmouseup
             */
            'itemmouseup',

            /**
             * @event itemmouseenter
             * @inheritdoc Ext.view.View#itemmouseenter
             */
            'itemmouseenter',

            /**
             * @event itemmouseleave
             * @inheritdoc Ext.view.View#itemmouseleave
             */
            'itemmouseleave',

            /**
             * @event itemclick
             * @inheritdoc Ext.view.View#itemclick
             */
            'itemclick',

            /**
             * @event itemdblclick
             * @inheritdoc Ext.view.View#itemdblclick
             */
            'itemdblclick',

            /**
             * @event itemcontextmenu
             * @inheritdoc Ext.view.View#itemcontextmenu
             */
            'itemcontextmenu',

            /**
             * @event beforecellclick
             * @inheritdoc Ext.view.Table#beforecellclick
             */
            'beforecellclick',

            /**
             * @event cellclick
             * @inheritdoc Ext.view.Table#cellclick
             */
            'cellclick',

            /**
             * @event beforecelldblclick
             * @inheritdoc Ext.view.Table#beforecelldblclick
             */
            'beforecelldblclick',

            /**
             * @event celldblclick
             * @inheritdoc Ext.view.Table#celldblclick
             */
            'celldblclick',

            /**
             * @event beforecellcontextmenu
             * @inheritdoc Ext.view.Table#beforecellcontextmenu
             */
            'beforecellcontextmenu',

            /**
             * @event cellcontextmenu
             * @inheritdoc Ext.view.Table#cellcontextmenu
             */
            'cellcontextmenu',

            /**
             * @event beforecellmousedown
             * @inheritdoc Ext.view.Table#beforecellmousedown
             */
            'beforecellmousedown',

            /**
             * @event cellmousedown
             * @inheritdoc Ext.view.Table#cellmousedown
             */
            'cellmousedown',

            /**
             * @event beforecellmouseup
             * @inheritdoc Ext.view.Table#beforecellmouseup
             */
            'beforecellmouseup',

            /**
             * @event cellmouseup
             * @inheritdoc Ext.view.Table#cellmouseup
             */
            'cellmouseup',

            /**
             * @event beforecellkeydown
             * @inheritdoc Ext.view.Table#beforecellkeydown
             */
            'beforecellkeydown',

            /**
             * @event cellkeydown
             * @inheritdoc Ext.view.Table#cellkeydown
             */
            'cellkeydown',

            /**
             * @event rowclick
             * @inheritdoc Ext.view.Table#rowclick
             */
            'rowclick',

            /**
             * @event rowdblclick
             * @inheritdoc Ext.view.Table#rowdblclick
             */
            'rowdblclick',

            /**
             * @event rowcontextmenu
             * @inheritdoc Ext.view.Table#rowcontextmenu
             */
            'rowcontextmenu',

            /**
             * @event rowmousedown
             * @inheritdoc Ext.view.Table#rowmousedown
             */
            'rowmousedown',

            /**
             * @event rowmouseup
             * @inheritdoc Ext.view.Table#rowmouseup
             */
            'rowmouseup',

            /**
             * @event rowkeydown
             * @inheritdoc Ext.view.Table#rowkeydown
             */
            'rowkeydown',

            /**
             * @event beforeitemkeydown
             * @inheritdoc Ext.view.View#event!beforeitemkeydown
             */
            'beforeitemkeydown',

            /**
             * @event itemkeydown
             * @inheritdoc Ext.view.View#event!itemkeydown
             */
            'itemkeydown',

            /**
             * @event beforeitemkeyup
             * @inheritdoc Ext.view.View#event!beforeitemkeyup
             */
            'beforeitemkeyup',

            /**
             * @event itemkeyup
             * @inheritdoc Ext.view.View#event!itemkeyup
             */
            'itemkeyup',

            /**
             * @event beforeitemkeypress
             * @inheritdoc Ext.view.View#event!beforeitemkeypress
             */
            'beforeitemkeypress',

            /**
             * @event itemkeypress
             * @inheritdoc Ext.view.View#event!itemkeypress
             */
            'itemkeypress',

            /**
             * @event beforecontainermousedown
             * @inheritdoc Ext.view.View#beforecontainermousedown
             */
            'beforecontainermousedown',

            /**
             * @event beforecontainermouseup
             * @inheritdoc Ext.view.View#beforecontainermouseup
             */
            'beforecontainermouseup',

            /**
             * @event beforecontainermouseover
             * @inheritdoc Ext.view.View#beforecontainermouseover
             */
            'beforecontainermouseover',

            /**
             * @event beforecontainermouseout
             * @inheritdoc Ext.view.View#beforecontainermouseout
             */
            'beforecontainermouseout',

            /**
             * @event beforecontainerclick
             * @inheritdoc Ext.view.View#beforecontainerclick
             */
            'beforecontainerclick',

            /**
             * @event beforecontainerdblclick
             * @inheritdoc Ext.view.View#beforecontainerdblclick
             */
            'beforecontainerdblclick',

            /**
             * @event beforecontainercontextmenu
             * @inheritdoc Ext.view.View#beforecontainercontextmenu
             */
            'beforecontainercontextmenu',

            /**
             * @event beforecontainerkeydown
             * @inheritdoc Ext.view.View#beforecontainerkeydown
             */
            'beforecontainerkeydown',

            /**
             * @event beforecontainerkeyup
             * @inheritdoc Ext.view.View#beforecontainerkeyup
             */
            'beforecontainerkeyup',

            /**
             * @event beforecontainerkeypress
             * @inheritdoc Ext.view.View#beforecontainerkeypress
             */
            'beforecontainerkeypress',

            /**
             * @event containermouseup
             * @inheritdoc Ext.view.View#containermouseup
             */
            'containermouseup',

            /**
             * @event containermousedown
             * @inheritdoc Ext.view.View#containermousedown
             */
            'containermousedown',

            /**
             * @event containermouseover
             * @inheritdoc Ext.view.View#containermouseover
             */
            'containermouseover',

            /**
             * @event containermouseout
             * @inheritdoc Ext.view.View#containermouseout
             */
            'containermouseout',

            /**
             * @event containerclick
             * @inheritdoc Ext.view.View#containerclick
             */
            'containerclick',

            /**
             * @event containerdblclick
             * @inheritdoc Ext.view.View#containerdblclick
             */
            'containerdblclick',

            /**
             * @event containercontextmenu
             * @inheritdoc Ext.view.View#containercontextmenu
             */
            'containercontextmenu',

            /**
             * @event containerkeydown
             * @inheritdoc Ext.view.View#containerkeydown
             */
            'containerkeydown',

            /**
             * @event containerkeyup
             * @inheritdoc Ext.view.View#containerkeyup
             */
            'containerkeyup',

            /**
             * @event containerkeypress
             * @inheritdoc Ext.view.View#containerkeypress
             */
            'containerkeypress',

            /**
             * @event beforeselect
             * @inheritdoc Ext.selection.RowModel#beforeselect
             */
            'beforeselect',

            /**
             * @event select
             * @inheritdoc Ext.selection.RowModel#select
             */
            'select',

            /**
             * @event beforedeselect
             * @inheritdoc Ext.selection.RowModel#beforedeselect
             */
            'beforedeselect',

            /**
             * @event deselect
             * @inheritdoc Ext.selection.RowModel#deselect
             */
            'deselect',

            /**
             * @event beforerowexit
             * @inheritdoc Ext.view.Table#beforerowexit
             */
            'beforerowexit'
        ]);

        // Only relay the event if it's not SpreadsheetModel.
        // SpreadsheetModel fires it directly through the Panel.
        if (!me.selModel.isSpreadsheetModel) {
            me.relayEvents(me.view, [
                /**
                 * @event selectionchange
                 * @inheritdoc Ext.selection.Model#selectionchange
                 */
                'selectionchange'
            ]);
        }

        // If we have our own headerCt (not gone through injectLockable), then add it
        // to our docked items and then add the columns. In this way, the columns
        // will immediately be able to interrogate their environment through getView
        // and getRootHeaderCt
        if (headerCt) {
            headerCt.view = me.view;
            (me.dockedItems = Ext.Array.from(me.dockedItems, true)).unshift(headerCt);
            headerCt.add(columns);
        }

        // Maintain backward compatibiliy by providing the initial leaf column set as a property.
        me.columns = me.headerCt.getGridColumns();

        me.callParent();

        me.syncHeaderVisibility();

        if (me.enableLocking) {
            me.afterInjectLockable();
        }

        me.addStateEvents([
            'columnresize',
            'columnmove',
            'columnhide',
            'columnshow',
            'sortchange',
            'filteractivate',
            'filterdeactivate',
            'filterchange',
            'groupchange'
        ]);

        // rowBody feature events
        /**
         * @event beforerowbodymousedown
         * @preventable
         * @inheritdoc Ext.view.Table#event-beforerowbodymousedown
         */

        /**
         * @event beforerowbodymouseup
         * @preventable
         * @inheritdoc Ext.view.Table#event-beforerowbodymouseup
         */

        /**
         * @event beforerowbodyclick
         * @preventable
         * @inheritdoc Ext.view.Table#event-beforerowbodyclick
         */

        /**
         * @event beforerowbodydblclick
         * @preventable
         * @inheritdoc Ext.view.Table#event-beforerowbodydblclick
         */

        /**
         * @event beforerowbodycontextmenu
         * @preventable
         * @inheritdoc Ext.view.Table#event-beforerowbodycontextmenu
         */

        /**
         * @event beforerowbodylongpress
         * @preventable
         * @inheritdoc Ext.view.Table#event-beforerowbodylongpress
         */

        /**
         * @event beforerowbodykeydown
         * @preventable
         * @inheritdoc Ext.view.Table#event-beforerowbodykeydown
         */

        /**
         * @event beforerowbodykeyup
         * @preventable
         * @inheritdoc Ext.view.Table#event-beforerowbodykeyup
         */

        /**
         * @event beforerowbodykeypress
         * @preventable
         * @inheritdoc Ext.view.Table#event-beforerowbodykeypress
         */

        /**
         * @event rowbodymousedown
         * @inheritdoc Ext.view.Table#event-rowbodymousedown
         */

        /**
         * @event rowbodymouseup
         * @inheritdoc Ext.view.Table#event-rowbodymouseup
         */

        /**
         * @event rowbodyclick
         * @inheritdoc Ext.view.Table#event-rowbodyclick
         */

        /**
         * @event rowbodydblclick
         * @inheritdoc Ext.view.Table#event-rowbodydblclick
         */

        /**
         * @event rowbodycontextmenu
         * @inheritdoc Ext.view.Table#event-rowbodycontextmenu
         */

        /**
         * @event rowbodylongpress
         * @inheritdoc Ext.view.Table#event-rowbodylongpress
         */

        /**
         * @event rowbodykeydown
         * @inheritdoc Ext.view.Table#event-rowbodykeydown
         */

        /**
         * @event rowbodykeyup
         * @inheritdoc Ext.view.Table#event-rowbodykeyup
         */

        /**
         * @event rowbodykeypress
         * @inheritdoc Ext.view.Table#event-rowbodykeypress
         */
    },

    updateHideHeaders: function(hideHeaders) {
        // Must only update the visibility after all configuration is finished.
        // initComponent calls syncHeaderVisibility
        if (!this.isConfiguring) {
            this.syncHeaderVisibility();
        }
    },

    beforeRender: function() {
        var me = this,
            bufferedRenderer = me.bufferedRenderer,
            ariaAttr;

        // If this is the topmost container of a lockable assembly, add the special class body
        if (me.lockable) {
            me.getProtoBody().addCls(me.lockingBodyCls);
        }

        // Don't create a buffered renderer for a locked grid.
        else {
            // If we're auto heighting, we can't buffered render, so don't create it
            if (bufferedRenderer && me.getSizeModel().height.auto) {
                //<debug>
                if (bufferedRenderer.isBufferedRenderer) {
                    Ext.raise('Cannot use buffered rendering with auto height');
                }
                //</debug>

                me.bufferedRenderer = bufferedRenderer = false;
            }

            if (bufferedRenderer && !bufferedRenderer.isBufferedRenderer) {
                // Create a BufferedRenderer as a plugin if we have not already configured with one.
                bufferedRenderer = {
                    xclass: 'Ext.grid.plugin.BufferedRenderer'
                };

                // eslint-disable-next-line max-len
                Ext.copy(bufferedRenderer, me, 'variableRowHeight,numFromEdge,trailingBufferZone,leadingBufferZone,scrollToLoadBuffer', true);

                me.bufferedRenderer = me.addPlugin(bufferedRenderer);
            }

            ariaAttr = me.ariaRenderAttributes || (me.ariaRenderAttributes = {});
            ariaAttr['aria-readonly'] = !me.isEditable;
            ariaAttr['aria-multiselectable'] = me.selModel.selectionMode !== 'SINGLE';
        }

        me.callParent(arguments);
    },

    beforeLayout: function() {
        var lockable = this.mixins.lockable;

        if (lockable) {
            lockable.beforeLayout.call(this);
        }

        this.callParent();
    },

    onHide: function(animateTarget, cb, scope) {
        this.getView().onOwnerGridHide();
        this.callParent([animateTarget, cb, scope]);
    },

    onShow: function() {
        this.callParent();
        this.getView().onOwnerGridShow();
    },

    /**
     * Gets the {@link Ext.grid.header.Container headercontainer} for this grid / tree.
     * @return {Ext.grid.header.Container} headercontainer
     *
     * **Note:** While a locked grid / tree will return an instance of
     * {@link Ext.grid.locking.HeaderContainer} you will code to the
     * {@link Ext.grid.header.Container} API.
     */
    getHeaderContainer: function() {
        return this.getView().getHeaderCt();
    },

    /**
     * @method getColumns
     * @inheritdoc Ext.grid.header.Container#getGridColumns
     */
    getColumns: function() {
        return this.getColumnManager().getColumns();
    },

    /**
     * @method getVisibleColumns
     * @inheritdoc Ext.grid.header.Container#getVisibleGridColumns
     */
    getVisibleColumns: function() {
        return this.getVisibleColumnManager().getColumns();
    },

    getScrollable: function() {
        // Lockable grids own a separate Y scroller which scrolls both grids in a single
        // scrolling element.
        // Regular grids return their view's scroller.
        return this.scrollable || this.view.getScrollable();
    },

    focus: function() {
        // TablePanel is not focusable, but allow a call to delegate into the view
        var view = this.getView();

        if (!view.isVisible(true)) {
            return false;
        }

        view.focus();
    },

    /**
     * Disables interaction with, and masks this grid's column headers.
     */
    disableColumnHeaders: function() {
        this.headerCt.disable();
    },

    /**
     * Enables interaction with, and unmasks this grid's column headers after a call
     * to {#disableColumnHeaders}.
     */
    enableColumnHeaders: function() {
        this.headerCt.enable();
    },

    /**
     * @private
     * Determine if there are any columns with a locked configuration option.
     */
    hasLockedColumns: function(columns) {
        var i, len, column;

        // Fully instantiated HeaderContainer
        if (columns.isRootHeader) {
            columns = columns.items.items;
        }
        // Config object with items
        else if (Ext.isObject(columns)) {
            columns = columns.items;
        }

        for (i = 0, len = columns.length; i < len; i++) {
            column = columns[i];

            if (!column.processed && column.locked) {
                return true;
            }
        }
    },

    relayHeaderCtEvents: function(headerCt) {
        this.relayEvents(headerCt, [
            /**
             * @event columnresize
             * @inheritdoc Ext.grid.header.Container#columnresize
             */
            'columnresize',

            /**
             * @event columnmove
             * @inheritdoc Ext.grid.header.Container#columnmove
             */
            'columnmove',

            /**
             * @event columnhide
             * @inheritdoc Ext.grid.header.Container#columnhide
             */
            'columnhide',

            /**
             * @event columnshow
             * @inheritdoc Ext.grid.header.Container#columnshow
             */
            'columnshow',

            /**
             * @event columnschanged
             * @inheritdoc Ext.grid.header.Container#columnschanged
             */
            'columnschanged',

            /**
             * @event sortchange
             * @inheritdoc Ext.grid.header.Container#sortchange
             */
            'sortchange',

            /**
             * @event headerclick
             * @inheritdoc Ext.grid.header.Container#headerclick
             */
            'headerclick',

            /**
             * @event headercontextmenu
             * @inheritdoc Ext.grid.header.Container#headercontextmenu
             */
            'headercontextmenu',

            /**
             * @event headertriggerclick
             * @inheritdoc Ext.grid.header.Container#headertriggerclick
             */
            'headertriggerclick'
        ]);
    },

    getState: function() {
        var me = this,
            state = me.callParent(),
            storeState = me.store.getState();

        state = me.addPropertyToState(state, 'columns', me.headerCt.getColumnsState());

        if (storeState) {
            state.storeState = storeState;
        }

        return state;
    },

    applyState: function(state) {
        var me = this,
            sorter = state.sort,
            storeState = state.storeState,
            store = me.store,
            columns = state.columns = me.buildColumnHash(state.columns);

        // Ensure superclass has applied *its* state.
        // Component saves dimensions (and anchor/flex) plus collapsed state.
        me.callParent([state]);

        if (columns) {
            // Column state restoration needs to examine store state
            me.headerCt.applyColumnsState(columns, storeState);
        }

        if (store.isEmptyStore) {
            return;
        }

        // Old stored sort state. Deprecated and will die out.
        if (sorter) {
            if (store.getRemoteSort()) {
                // Pass false to prevent a sort from occurring.
                store.sort({
                    property: sorter.property,
                    direction: sorter.direction,
                    root: sorter.root
                }, null, false);
            }
            else {
                store.sort(sorter.property, sorter.direction);
            }
        }
        // New storeState which encapsulates groupers, sorters and filters.
        else if (storeState) {
            store.applyState(storeState);
        }
    },

    buildColumnHash: function(columns) {
        var len, columnState, i, result;

        // Create a usable state lookup hash from which each column
        // may look up its state based upon its stateId
        // {
        //      col_name: {
        //          index: 0,
        //          width: 100,
        //          locked: true
        //      },
        //      col_details: {
        //          index: 1,
        //          width: 200,
        //          columns: {
        //              col_details_1: {
        //                  index: 0,
        //                  width: 100
        //              },
        //              col_details_2: {
        //                  index: 1,
        //                  width: 100
        //              }
        //          }
        //      },
        // }
        if (columns) {
            result = {};

            for (i = 0, len = columns.length; i < len; i++) {
                columnState = columns[i];
                columnState.index = i;

                if (columnState.columns) {
                    columnState.columns = this.buildColumnHash(columnState.columns);
                }

                result[columnState.id] = columnState;
            }

            return result;
        }
    },

    /**
     * Returns the store associated with this Panel.
     * @return {Ext.data.Store} The store
     */
    getStore: function() {
        return this.store;
    },

    onViewRefresh: function(view, records) {
        this.onItemsAdded(view, records, 0);
    },

    onItemAdd: function(records, index, nodes, view) {
        this.onItemsAdded(view, records, index);
    },

    onItemsAdded: function(view, records, index) {
        var me = this,
            recCount = records.length,
            freeRowContexts = me.freeRowContexts,
            liveRowContexts = me.liveRowContexts || (me.liveRowContexts = {}),
            i, internalId, rowContext, record;

        // Ensure we have RowContexts ready for all the widget owners
        // (Widget columns or RowWidget plugin) which will be needing instantiated
        // Widgets with attached ViewModels.
        for (i = 0; i < recCount; i++) {
            internalId = (record = records[i]).internalId;

            // We may have already been informed about the addition of this item
            // by the opposite locking partner
            if (!(rowContext = liveRowContexts[internalId])) {
                // Attempt to read from free RowContexts which may have been freed
                // by a previous item remove event. Shift of the front
                // to improve the chances of using the same RowContext for a record;
                // They were pushed on in the item remove handler.
                rowContext = freeRowContexts && freeRowContexts.shift();

                // Need a new one
                if (!rowContext) {
                    rowContext = new Ext.grid.RowContext({
                        ownerGrid: me
                    });
                }

                if (rowContext.attach(view)) {
                    // if this is the first view to attach, initialize the context and
                    // put it in the live set:
                    me.liveRowContexts[internalId] = rowContext;
                    rowContext.setRecord(record, index + i);
                }
            }
            else {
                rowContext.attach(view);
            }
        }
    },

    onItemRemove: function(records, index, nodes, view) {
        var me = this,
            freeRowContexts = me.freeRowContexts || (me.freeRowContexts = []),
            liveRowContexts = me.liveRowContexts,
            len = nodes.length,
            i, id, context;

        for (i = 0; i < len; i++) {
            id = nodes[i].getAttribute('data-recordId');
            context = liveRowContexts[id];

            if (context && context.detach(view)) {
                // if this is the last view to detach, return the context to the free
                // list.
                freeRowContexts.push(context);
                delete liveRowContexts[id];
            }
        }
    },

    createManagedWidget: function(view, ownerId, widgetConfig, record) {
        return this.liveRowContexts[record.internalId].getWidget(view, ownerId, widgetConfig);
    },

    destroyManagedWidgets: function(ownerId) {
        var me = this,
            contexts = me.liveRowContexts,
            freeRowContexts = me.freeRowContexts,
            len = freeRowContexts && freeRowContexts.length,
            i, recInternalId, rowWidgets;

        // Destroy widgets from both live contexts, and free ones
        for (recInternalId in contexts) {
            rowWidgets = contexts[recInternalId].widgets;

            if (rowWidgets) {
                Ext.destroy(rowWidgets[ownerId]);
                delete rowWidgets[ownerId];
            }
        }

        for (i = 0; i < len; i++) {
            rowWidgets = freeRowContexts[i].widgets;

            if (rowWidgets) {
                Ext.destroy(rowWidgets[ownerId]);
                delete rowWidgets[ownerId];
            }
        }
    },

    getManagedWidgets: function(ownerId) {
        var me = this,
            contexts = me.liveRowContexts,
            recInternalId,
            result = [];

        for (recInternalId in contexts) {
            result.push(contexts[recInternalId].widgets[ownerId]);
        }

        return result;
    },

    /**
     * Gets the view for this panel.
     * @return {Ext.view.Table}
     */
    getView: function() {
        var me = this,
            scroll, scrollable, viewConfig;

        if (!me.view) {
            viewConfig = me.viewConfig;
            scroll = viewConfig.scroll || me.scroll;

            scrollable = me.scrollable;

            if (scrollable == null && viewConfig.scrollable == null && scroll !== null) {
                // transform deprecated scroll config into scrollable config
                if (scroll === true || scroll === 'both') {
                    scrollable = true;
                }
                else if (scroll === false || scroll === 'none') {
                    scrollable = false;
                }
                else if (scroll === 'vertical') {
                    scrollable = {
                        x: false,
                        y: true
                    };
                }
                else if (scroll === 'horizontal') {
                    scrollable = {
                        x: true,
                        y: false
                    };
                }
            }

            viewConfig = Ext.apply({
                // TableView injects the view reference into this grid so that we have a reference
                // as early as possible and Features need a reference to the grid.
                // For these reasons, we configure a reference to this grid into the View
                grid: me,
                ownerGrid: me.ownerGrid,
                deferInitialRefresh: me.deferRowRender,
                variableRowHeight: me.variableRowHeight,
                preserveScrollOnRefresh: true,
                trackOver: me.trackMouseOver !== false,
                throttledUpdate: me.throttledUpdate === true,
                xtype: me.viewType,
                store: me.store,
                headerCt: me.headerCt,
                columnLines: me.columnLines,
                rowLines: me.rowLines,
                navigationModel: 'grid',
                features: me.features,
                panel: me,
                emptyText: me.emptyText || ''
            }, me.viewConfig);

            // Impose our calculated scrollable config only if scrollability is not configured.
            // eslint-disable-next-line max-len
            if (!('scrollable' in viewConfig || 'scroll' in viewConfig || 'autoScroll' in viewConfig) && scrollable != null) {
                viewConfig.scrollable = scrollable;
            }

            viewConfig.$initParent = me;
            Ext.create(viewConfig);
            delete viewConfig.$initParent;

            // Normalize the application of the markup wrapping the emptyText config.
            // `emptyText` can now be defined on the grid as well as on its viewConfig,
            // and this led to the emptyText not having the wrapping markup when it was defined
            // in the viewConfig. It should be backwards compatible.
            // Note that in the unlikely event that emptyText is defined on both the grid config
            // and the viewConfig that the viewConfig wins.
            if (me.view.emptyText) {
                me.view.emptyText = '<div class="' + me.emptyCls + '">' +
                                    me.view.emptyText + '</div>';
            }

            // TableView's custom component layout, Ext.view.TableLayout requires a reference
            // to the headerCt because it depends on the headerCt doing its work.
            me.view.getComponentLayout().headerCt = me.headerCt;

            me.mon(me.view, {
                uievent: me.processEvent,
                scope: me
            });

            // Plugins and features may need to access the view as soon as it is created.
            if (me.hasListeners.viewcreated) {
                me.fireEvent('viewcreated', me, me.view);
            }
        }

        return me.view;
    },

    getEmptyText: function() {
        return this.view.emptyText;
    },

    setEmptyText: function(emptyText) {
        this.emptyText = emptyText;

        this.view.setEmptyText(
            '<div class="' + this.emptyCls + '">' + emptyText + '</div>'
        );

        return this;
    },

    getColumnManager: function() {
        return this.columnManager;
    },

    getVisibleColumnManager: function() {
        return this.visibleColumnManager;
    },

    getTopLevelColumnManager: function() {
        return this.ownerGrid.getColumnManager();
    },

    getTopLevelVisibleColumnManager: function() {
        return this.ownerGrid.getVisibleColumnManager();
    },

    /**
     * @method setAutoScroll
     */
    setAutoScroll: Ext.emptyFn,

    applyScrollable: function(scrollable) {
        var view = this.view;

        view = view && (view.normalView || view);

        if (view) {
            view.setScrollable(scrollable);
        }

        // The view might not yet exists so we just stash the raw config away so it
        // can be processed by getView()
        return scrollable;
    },

    /**
     * @private
     * Processes UI events from the view. Propagates them to whatever internal Components
     * need to process them.
     * @param {String} type Event type, eg 'click'
     * @param {Ext.view.Table} view TableView Component
     * @param {HTMLElement} cell Cell HTMLElement the event took place within
     * @param {Number} recordIndex Index of the associated Store Model (-1 if none)
     * @param {Number} cellIndex Cell index within the row
     * @param {Ext.event.Event} e Original event
     * @param {Ext.data.Model} record
     * @param {Object} row
     */
    processEvent: function(type, view, cell, recordIndex, cellIndex, e, record, row) {
        var header = e.position.column;

        if (header) {
            return header.processEvent.apply(header, arguments);
        }
    },

    /**
     * Scrolls the specified record into view.
     * @param {Number/String/Ext.data.Model} record The record, record id, or the zero-based
     * position in the dataset to scroll to.
     * @param {Object} [options] An object containing options to modify the operation.
     * @param {Number/Ext.grid.column.Column} [options.column] The column to scroll into view.
     * @param {Boolean} [options.animate] Pass `true` to animate the row into view.
     * @param {Boolean} [options.highlight] Pass `true` to highlight the row with a glow animation
     * when it is in view.
     * @param {Boolean} [options.select] Pass as `true` to select the specified row.
     * @param {Boolean} [options.focus] Pass as `true` to focus the specified row.
     * @param {Function} [options.callback] A function to execute when the record is in view.
     * This may be necessary if the first parameter is a record index and the view is backed by a
     * {@link Ext.data.BufferedStore buffered store} which does not contain that record.
     * @param {Boolean} options.callback.success `true` if acquiring the record's view node
     * was successful.
     * @param {Ext.data.Model}  options.callback.record If successful, the target record.
     * @param {HTMLElement} options.callback.node If successful, the record's view node.
     * @param {Object} [options.scope] The scope (`this` reference) in which the callback function
     * is executed.
     */
    ensureVisible: function(record, options) {
        this.doEnsureVisible(record, options);
    },

    scrollByDeltaY: function(yDelta, animate) {
        // xDelta should be null here not 0! We're not scrolling horizontally,
        // and the Scroller is sensitive to these things.
        this.getView().scrollBy(null, yDelta, animate);
    },

    scrollByDeltaX: function(xDelta, animate) {
        // Ditto yDelta.
        this.getView().scrollBy(xDelta, null, animate);
    },

    afterCollapse: function() {
        this.saveScrollPos();
        this.callParent(arguments);
    },

    afterExpand: function() {
        this.callParent(arguments);
        this.restoreScrollPos();
    },

    saveScrollPos: Ext.emptyFn,

    restoreScrollPos: Ext.emptyFn,

    onHeaderResize: Ext.emptyFn,

    // Update the view when a header moves
    onHeaderMove: function(headerCt, header, colsToMove, fromIdx, toIdx) {
        var me = this;

        // If there are Features or Plugins which create DOM which must match column order,
        // they set the optimizedColumnMove flag to false.
        // In this case we must refresh the view on column move.
        if (me.optimizedColumnMove === false) {
            me.view.refreshView();
        }

        // Simplest case for default DOM structure is just to swap the columns round in the view.
        else {
            me.view.moveColumn(fromIdx, toIdx, colsToMove);
        }

        me.delayScroll();
    },

    // Section onHeaderHide is invoked after view.
    onHeaderHide: function(headerCt, header) {
        var view = this.view;

        // The headerCt may be hiding multiple children if a leaf level column
        // causes a parent (and possibly other parents) to be hidden. Only run the refresh
        // once we're done
        if (!headerCt.childHideCount && view.refreshCounter) {
            view.refreshView();
        }
    },

    onHeaderShow: function(headerCt, header) {
        var view = this.view;

        if (view.refreshCounter) {
            view.refreshView();
        }
    },

    // To be triggered on add/remove/move for a leaf header
    onHeadersChanged: function(headerCt, header) {
        var me = this;

        if (me.rendered && !me.reconfiguring) {
            me.view.refreshView();
            me.delayScroll();
        }
    },

    delayScroll: function() {
        var target = this.view;

        if (target) {
            // Do not cause a layout by reading scrollX now.
            // It must be read from the target when the task finally executes.
            this.scrollTask.delay(10, null, null, [target]);
        }
    },

    /**
     * @private
     * Fires the TablePanel's viewready event when the view declares that its internal DOM is ready
     */
    onViewReady: function() {
        this.fireEvent('viewready', this);
    },

    /**
     * @private
     * Tracks when things happen to the view and preserves the horizontal scroll position.
     */
    onRestoreHorzScroll: function() {
        var me = this,
            x = me.scrollXPos;

        if (x) {
            // We need to restore the body scroll position here
            me.syncHorizontalScroll(me, true);
        }
    },

    getScrollerOwner: function() {
        var rootCmp = this;

        if (!this.scrollerOwner) {
            rootCmp = this.up('[scrollerOwner]');
        }

        return rootCmp;
    },

    /**
     * Gets left hand side marker for header resizing.
     * @private
     */
    getLhsMarker: function() {
        var me = this;

        return me.lhsMarker || (me.lhsMarker = Ext.DomHelper.append(me.el, {
            role: 'presentation',
            cls: me.resizeMarkerCls
        }, true));
    },

    /**
     * Gets right hand side marker for header resizing.
     * @private
     */
    getRhsMarker: function() {
        var me = this;

        return me.rhsMarker || (me.rhsMarker = Ext.DomHelper.append(me.el, {
            role: 'presentation',
            cls: me.resizeMarkerCls
        }, true));
    },

    /**
     * @method getSelection
     * Returns the grid's selection. See `{@link Ext.selection.Model#getSelection}`.
     * @inheritdoc Ext.selection.Model#getSelection
     */
    getSelection: function() {
        return this.getSelectionModel().getSelection();
    },

    /**
     * Sets the value of the selection.
     * @param {Ext.data.Model} selection
     */
    setSelection: function(selection) {
        // This is purposefully written not as a config. Because getSelection
        // is an existing API that doesn't mirror the value for setSelection, we
        // don't want the publish system to call the getter, but rather just the
        // raw property.
        var current = this.selection;

        if (selection !== current) {
            this.selection = selection;
            this.updateSelection(selection, current);
        }
    },

    updateSelection: function(selection) {
        var me = this,
            sm;

        if (!me.ignoreNextSelection) {
            me.ignoreNextSelection = true;
            sm = me.getSelectionModel();

            if (selection) {
                sm.select(selection);
            }
            else {
                sm.deselectAll();
            }

            me.ignoreNextSelection = false;
        }

        me.publishState('selection', selection);
    },

    updateBindSelection: function(selModel, selection) {
        var me = this,
            hasSelection = selection.length > 0,
            selected = null;

        me.hasHadSelection = me.hasHadSelection || hasSelection;

        if (!me.ignoreNextSelection) {
            me.ignoreNextSelection = true;

            if (hasSelection) {
                selected = selModel.getLastSelected();
            }

            if (me.hasHadSelection) {
                me.setSelection(selected);
            }

            me.ignoreNextSelection = false;
        }
    },

    updateFocused: function(record) {
        this.getNavigationModel().setPosition(record);
    },

    updateHeaderBorders: function(headerBorders) {
        this[headerBorders ? 'removeCls' : 'addCls'](this.noHeaderBordersCls);
    },

    getNavigationModel: function() {
        return this.getView().getNavigationModel();
    },

    /**
     * Returns the selection model being used by this grid's {@link Ext.view.Table view}.
     * @return {Ext.selection.Model} The selection model being used by this grid's
     * {@link Ext.view.Table view}.
     */
    getSelectionModel: function() {
        return this.getView().getSelectionModel();
    },

    getScrollTarget: function() {
        var items = this.getScrollerOwner().query('tableview');

        // Last view has the scroller
        return items[items.length - 1];
    },

    syncHorizontalScroll: function(target, setBody) {
        var me = this,
            x = me.view.getScrollX(),
            scrollTarget;

        setBody = setBody === true;

        // Only set the horizontal scroll if we've changed position,
        // so that we don't set this on vertical scrolls
        if (me.rendered && (setBody || x !== me.scrollXPos)) {
            // Only set the body position if we're reacting to a refresh, otherwise
            // we just need to set the header.
            if (setBody) {
                scrollTarget = me.getScrollTarget();
                scrollTarget.setScrollX(x);
            }

            me.headerCt.setScrollX(x);
            me.scrollXPos = x;
        }
    },

    // template method meant to be overriden
    onStoreLoad: Ext.emptyFn,

    getEditorParent: function() {
        return this.body;
    },

    bindStore: function(store, initial) {
        var me = this,
            view = me.getView(),
            oldStore = me.getStore();

        // Normally, this method will always be called with a valid store (because there is
        // a symmetric .unbindStore method), but there are cases where this method will be called
        // and passed a null value, i.e., a panel is used as a pickerfield. See EXTJS-13089.
        if (store) {
            // Bind to store immediately because subsequent processing
            // looks for grid's store property
            me.store = store;

            if (view.store !== store) {
                // If coming from a reconfigure, we need to set the actual store property
                // on the view. Setting the store will then also set the dataSource.
                //
                // Note that if it's a grid feature then this is sorted out in view.bindStore(),
                // and its own implementation of .bindStore() will be called.
                view.bindStore(store, false);
            }

            me.mon(store, {
                load: me.onStoreLoad,
                scope: me
            });

            me.storeRelayers = me.relayEvents(store, [
                /**
                 * @event filterchange
                 * @inheritdoc Ext.data.Store#filterchange
                 */
                'filterchange',

                /**
                 * @event groupchange
                 * @inheritdoc Ext.data.Store#groupchange
                 */
                'groupchange'
            ]);

            // If this is being called from reconfigure then the storechange will be called
            // by the reconfigure machinery at the end of all processing. Otherwise, fire here.
            if (!me.reconfiguring && me.hasListeners.storechange && store !== oldStore) {
                me.fireEvent('storechange', me, store, oldStore);
            }
        }
        else {
            me.unbindStore();
        }
    },

    unbindStore: function() {
        var me = this,
            store = me.store,
            view;

        if (store) {
            store.trackStateChanges = false;
            me.store = null;

            me.mun(store, {
                load: me.onStoreLoad,
                scope: me
            });

            Ext.destroy(me.storeRelayers);

            view = me.view;

            if (view.store) {
                view.bindStore(null);
            }
            else if (!store.destroyed && store.autoDestroy) {
                store.destroy();
            }

            // If this is being called from reconfigure then the storechange will be called
            // by the reconfigure machinery at the end of all processing. Otherwise, fire here.
            if (!me.reconfiguring && me.hasListeners.storechange) {
                me.fireEvent('storechange', me, null, store);
            }
        }
    },

    setColumns: function(columns) {
        // If being reconfigured from zero columns to zero columns, skip operation.
        // This can happen if columns are being set from a binding and the initial value
        // of the bound data in the ViewModel is []
        if (columns.length || this.getColumnManager().getColumns().length) {
            this.reconfigure(undefined, columns);
        }
    },

    /**
     * A convenience method that fires {@link #event-reconfigure} with the store param.
     * To set the store AND change columns, use the {@link #method-reconfigure reconfigure method}.
     *
     * @param {Ext.data.Store} [store] The new store.
     */
    setStore: function(store) {
        var me = this;

        me.reconfigure(store, undefined, true);

        // If we are visible, load the store
        if (me.isVisible(true)) {
            if (store && me.autoLoad && !store.isEmptyStore &&
                !(store.loading || store.isLoaded())) {
                store.load();
            }
        }
        // Otherwise, ensure that we will load as soon as we become visible
        else if (!me.globalShowListener) {
            me.globalShowListener = Ext.GlobalEvents.on({
                show: me.onGlobalShow,
                scope: me,
                destroyable: true
            });
        }
    },

    onGlobalShow: function(comp) {
        var me = this,
            store = me.store;

        // If the global show caused this to be shown, then load
        // unless there's already a locked kicked off.
        if (comp === me || (comp.isAncestor(me) && me.isVisible(true))) {
            if (store && me.autoLoad && !store.isEmptyStore &&
                !(store.loading || store.isLoaded())) {
                store.load();
            }

            Ext.destroy(me.globalShowListener);
        }
    },

    /**
     * Reconfigures the grid or tree with a new store and/or columns. Stores and columns 
     * may also be passed as params.
     *
     *     grid.reconfigure(store, columns);
     *
     * Additionally, you can pass just a store or columns.
     *
     *     tree.reconfigure(store);
     *     // or
     *     grid.reconfigure(columns);
     *     // or
     *     tree.reconfigure(null, columns);
     *
     * If you're using locked columns, the {@link #enableLocking} config should be set 
     * to `true` before the reconfigure method is executed.
     *
     * @param {Ext.data.Store/Object} [store] The new store instance or store config. You can 
     * pass `null` if no new store.
     * @param {Object[]} [columns] An array of column configs
     * @param {Boolean} allowUnbind (private)
     * @param {Boolean} applyState (private) Allow components (such as pivot grid) to determine
     * if they want to update when the store is reconfigured
     */
    reconfigure: function(store, columns, allowUnbind, applyState) {
        var me = this,
            oldStore = me.store,
            headerCt = me.headerCt,
            lockable = me.lockable,
            oldColumns = headerCt ? headerCt.items.getRange() : me.columns,
            view = me.getView(),
            scroller, block, refreshCounter, storeChanged, columnsChanged, state, stateId,
            restoreFocus;

        // Allow optional store argument to be fully omitted, and the columns argument to be solo
        if (arguments.length === 1 && Ext.isArray(store)) {
            columns = store;
            store = null;
        }

        // Make copy in case the beforereconfigure listener mutates it.
        if (columns) {
            columns = Ext.Array.slice(columns);
        }

        me.reconfiguring = true;

        if (store) {
            store = Ext.StoreManager.lookup(store);
            storeChanged = store && store !== oldStore;
        }
        // Allow for nulling the store (convert to the empty store)
        else if (allowUnbind) {
            store = Ext.StoreManager.lookup('ext-empty-store');
            storeChanged = store !== oldStore;
        }

        me.fireEvent('beforereconfigure', me, store, columns, oldStore, oldColumns);

        Ext.suspendLayouts();

        if (me.rendered && me.layoutCounter && (scroller = me.getScrollable())) {
            scroller.scrollTo(0, 0);
        }

        if (lockable) {
            me.reconfigureLockable(store, columns, allowUnbind);
        }
        else {
            // Prevent the view from refreshing until we have resumed layouts
            // and any columns are rendered
            block = view.blockRefresh;
            view.blockRefresh = true;
            restoreFocus = view.saveFocusState();

            // Note that we need to process the store first in case one or more passed columns
            // (if there are any) have active gridfilters with values which would filter
            // the currently-bound store.
            if (storeChanged) {
                me.unbindStore();
                me.bindStore(store);
            }

            if (columns) {
                // new columns, delete scroll pos
                delete me.scrollXPos;
                headerCt.removeAll();
                headerCt.add(columns);
                columnsChanged = true;
            }

            headerCt.onOwnerGridReconfigure(storeChanged, columnsChanged);

            refreshCounter = view.refreshCounter;
        }

        if (me.stateful && applyState !== false) {
            stateId = me.getStateId();
            state = stateId && Ext.state.Manager.get(stateId);

            if (state) {
                me.applyState(state);
            }
        }

        Ext.resumeLayouts(true);
        me.reconfiguring = false;

        if (lockable) {
            me.afterReconfigureLockable();
        }
        else {
            view.blockRefresh = block;

            // If the layout resumption didn't trigger the view to refresh, do it here
            if (view.refreshCounter === refreshCounter) {
                view.refreshView();
                restoreFocus();
            }
        }

        me.fireEvent('reconfigure', me, store, columns, oldStore, oldColumns);
        delete me.reconfiguring;

        if (storeChanged) {
            me.fireEvent('storechange', me, store, oldStore);

            if (!oldStore.destroyed && oldStore.autoDestroy) {
                oldStore.destroy();
            }
        }
    },

    doDestroy: function() {
        var me = this,
            task = me.scrollTask,
            view = me.view;

        if (view) {
            view.destroying = true;
        }

        if (me.lockable) {
            me.destroyLockable();
        }

        if (task) {
            task.cancel();
        }

        // Need to destroy plugins here because they may have listeners on the View
        Ext.destroy(
            me.rowContextParent, me.plugins, me.focusEnterLeaveListeners,
            me.freeRowContents, Ext.Object.getValues(me.liveRowContexts),
            me.lhsMarker, me.rhsMarker
        );

        me.callParent();

        // Have to unbind the store this late because plugins and other things
        // may still need it until the very end.
        me.unbindStore();
    },

    getElementHeight: function(el) {
        var rect = this.preciseHeight &&
            el.getBoundingClientRect();

        return rect ? (rect.height || (rect.bottom - rect.top)) : el.offsetHeight;
    },

    getElementSize: function(el) {
        var rect = this.preciseHeight &&
            el.getBoundingClientRect();

        return {
            width: rect ? (rect.width || (rect.right - rect.left)) : el.offsetWidth,
            height: rect ? (rect.height || (rect.bottom - rect.top)) : el.offsetHeight
        };
    },

    privates: {
        // The focusable flag is set, but there is no focusable element.
        // Focus is delegated to the view by the focus implementation.
        initFocusableElement: function() {},

        doEnsureVisible: function(record, options) {
            // Handle the case where this is a lockable assembly
            if (this.lockable) {
                return this.ensureLockedVisible(record, options);
            }

            // Allow them to pass the record id.
            if (typeof record !== 'number' && !record.isEntity) {
                record = this.store.getById(record);
            }

            // eslint-disable-next-line vars-on-top
            var me = this,
                view = me.getView(),
                domNode = view.getNode(record),
                isLocking = me.ownerGrid.lockable,
                callback, scope, animate,
                highlight, select, doFocus, verticalScroller, column, cell, targetContext,
                internalCallback, scrollPromise;

            if (options) {
                callback = options.callback;
                scope = options.scope;
                animate = options.animate;
                highlight = options.highlight;
                select = options.select;
                doFocus = options.focus;
                column = options.column;
            }

            // Always supercede any prior deferred request
            if (me.deferredEnsureVisible) {
                me.deferredEnsureVisible.destroy();
            }

            // We have not yet run the layout.
            // Add this to the end of the first sizing process.
            // By using the resize event, we will come in AFTER any Component's onResize
            // and onBoxReady handling.
            if (!view.componentLayoutCounter) {
                me.deferredEnsureVisible = view.on({
                    resize: me.doEnsureVisible,
                    args: Ext.Array.slice(arguments),
                    scope: me,
                    single: true,
                    destroyable: true
                });

                return;
            }

            if (typeof column === 'number') {
                column = me.ownerGrid.getVisibleColumnManager().getColumns()[column];
            }

            // We found the DOM node associated with the record
            if (domNode) {
                if (!record.isEntity) {
                    record = view.getRecord(domNode);
                }

                verticalScroller = isLocking ? me.ownerGrid.getScrollable() : view.getScrollable();

                // Scrolling *may* be asynchronous if animation is used, so post-process
                // the target node in a callback.
                if (callback || select || doFocus) {
                    internalCallback = function() {
                        if (view && view.destroyed) {
                            return;
                        }

                        targetContext =
                            new Ext.grid.CellContext(view).setPosition(record, column || 0);

                        if (select) {
                            view.getSelectionModel().selectByPosition(targetContext);
                        }

                        if (doFocus) {
                            view.getNavigationModel().setPosition(targetContext);
                        }

                        Ext.callback(callback, scope || me, [true, record, domNode]);
                    };
                }

                if (verticalScroller) {
                    if (column) {
                        cell = Ext.fly(domNode).selectNode(column.getCellSelector());
                    }

                    // We're going to need two scrollers if we are locking, and we need
                    // to scroll horizontally. The whole arrangement of side by side views
                    // scrolls up and down. Each view itself scrolls horizontally.
                    if (isLocking && column) {
                        verticalScroller.ensureVisible(domNode, {
                            x: false
                        });

                        scrollPromise = view.getScrollable().ensureVisible(cell || domNode, {
                            animation: animate,
                            highlight: highlight
                        });
                    }
                    // No locking, it's simple - we just use the view's scroller
                    else {
                        scrollPromise = verticalScroller.ensureVisible(cell || domNode, {
                            animation: animate,
                            highlight: highlight,
                            x: !!column
                        });
                    }

                    if (scrollPromise && internalCallback) {
                        scrollPromise.then(internalCallback);
                    }
                }
            }
            // If we didn't find it, it's probably because of buffered rendering
            else if (view.bufferedRenderer) {
                view.bufferedRenderer.scrollTo(record, {
                    animate: animate,
                    highlight: highlight,
                    select: select,
                    focus: doFocus,
                    column: column,
                    callback: function(recordIdx, record, domNode) {
                        Ext.callback(callback, scope || me, [true, record, domNode]);
                    }
                });
            }
            else {
                Ext.callback(callback, scope || me, [false, null]);
            }
        },

        getFocusEl: function() {
            return this.getView().getFocusEl();
        },

        /**
         * Provide a single parent viewmodel for the grid so that any VM for
         * row contents share the same scheduler.
         * @return {Ext.app.ViewModel}
         *
         * @private
         */
        getRowContextViewModelParent: function() {
            var vm = this.lookupViewModel() || this.rowContextParent;

            if (!vm) {
                // If we get to this point, it means that there's no parent VM above us
                // so we have nothing to hook up to
                this.rowContextParent = vm = new Ext.app.ViewModel();
            }

            return vm;
        },

        handleWidgetViewChange: function(view, ownerId) {
            var contexts = this.liveRowContexts,
                freeRowContexts = this.freeRowContexts,
                len = freeRowContexts && freeRowContexts.length,
                i, recInternalId;

            for (recInternalId in contexts) {
                contexts[recInternalId].handleWidgetViewChange(view, ownerId);
            }

            for (i = 0; i < len; i++) {
                freeRowContexts[i].handleWidgetViewChange(view, ownerId);
            }
        },

        initInheritedState: function(inheritedState, inheritedStateInner) {
            inheritedState.inLockedGrid = !!this.isLocked;
            this.callParent([inheritedState, inheritedStateInner]);
        },

        /**
         * Toggles ARIA actionable mode on/off
         * @param {Boolean} enabled
         * @param {Ext.grid.CellContext} position The cell to activate.
         * @param {HTMLElement/Ext.dom.Element} [position.target] The element within
         * the referenced cell to focus.
         * @return {Boolean} `true` if actionable mode was entered
         * @private
         */
        setActionableMode: function(enabled, position) {
            // Always set the topmost grid in a lockable assembly
            var me = this.ownerGrid;

            // Can be called to exit actionable mode upon a focusLeave caused by destruction
            if (!me.destroying && me.view.setActionableMode(enabled, position) !== false) {
                me.fireEvent('actionablemodechange', enabled);
                me[enabled ? 'addCls' : 'removeCls'](me.actionableModeCls);

                return true;
            }
        },

        /**
         * Override for TablePanel.
         * A TablePanel can never scroll. Its View scrolls.
         * @private
         */
        getOverflowStyle: function() {
            // eslint-disable-next-line dot-notation
            this.scrollFlags = this._scrollFlags['false']['false'];

            return {
                overflowX: 'hidden',
                overflowY: 'hidden'
            };
        },

        getOverflowEl: function() {
            return null;
        },

        shouldAutoHideHeaders: function() {
            var me = this,
                columns = me.headerCt.items.items,
                len = columns.length,
                autoHideHeaders = !!len,
                column, i;

            // Loop until we find a column with content.
            for (i = 0; autoHideHeaders && i < len; i++) {
                column = columns[i];

                // If any column was configured with visible text, we must show headers.
                if (!column.isEmptyText(column.text, true) || column.columns ||
                    (column.isGroupHeader && column.items.items.length)) {
                    autoHideHeaders = false;
                }
            }

            return autoHideHeaders;
        },

        syncHeaderVisibility: function() {
            var me = this,
                headerCt = me.headerCt,
                hideHeaders = me.hideHeaders,
                viewScroller, currentHideHeaderState;

            if (me.lockable) {
                me.syncLockableHeaderVisibility();

                return;
            }

            if (hideHeaders == null) {
                hideHeaders = me.shouldAutoHideHeaders();
            }

            currentHideHeaderState = headerCt.height === 0;

            if (!headerCt.rendered || hideHeaders !== currentHideHeaderState) {
                headerCt.setHeight(hideHeaders ? 0 : null);
                headerCt.hiddenHeaders = hideHeaders;
                headerCt.toggleCls(me.hiddenHeaderCtCls, hideHeaders);
                me.toggleCls(me.hiddenHeaderCls, hideHeaders);

                if (!hideHeaders) {
                    headerCt.setScrollable({
                        x: false,
                        y: false
                    });

                    viewScroller = me.view.getScrollable();

                    if (viewScroller) {
                        headerCt.getScrollable().addPartner(viewScroller, 'x');
                    }
                }
            }
        }
    }
});
